Actividad 5#

Valentina Cabrera

Librerias#

import pandas as pd
from IPython.display import display
import seaborn as sns
import matplotlib.pyplot as plt
import warnings
import numpy as np
import plotly.express as px
import missingno as msno
import plotly.graph_objects as go
import geopandas as gpd 
from urllib.request import urlopen
from numpy import mean
from tabulate import tabulate
from scipy import stats
import folium
from folium import Choropleth, Popup, Tooltip
warnings.filterwarnings('ignore')

Accidentalidad en Barranquilla.#

Base de datos#

data = pd.read_csv('C:/Users/valec/Downloads/Escritorio/Accidentalidad_en_Barranquilla_20240826.csv')
display(data.head(10).style.set_caption("Base de datos: Accidentalidad en Barranquilla"))
Base de datos: Accidentalidad en Barranquilla
  FECHA_ACCIDENTE HORA_ACCIDENTE GRAVEDAD_ACCIDENTE CLASE_ACCIDENTE SITIO_EXACTO_ACCIDENTE CANT_HERIDOS_EN _SITIO_ACCIDENTE CANT_MUERTOS_EN _SITIO_ACCIDENTE CANTIDAD_ACCIDENTES AÑO_ACCIDENTE MES_ACCIDENTE DIA_ACCIDENTE
0 2018-01-01T00:00:00.000 01:30:00:am Con heridos Atropello CL 87 9H 24 1.000000 nan 1 2018 January Mon
1 2018-01-01T00:00:00.000 02:00:00:pm Solo daños Choque CL 110 CR 46 nan nan 1 2018 January Mon
2 2018-01-01T00:00:00.000 04:00:00:am Solo daños Choque AV CIRCUNVALAR CR 9G nan nan 1 2018 January Mon
3 2018-01-01T00:00:00.000 04:30:00:am Solo daños Choque CLLE 72 CRA 29 nan nan 1 2018 January Mon
4 2018-01-01T00:00:00.000 05:20:00:pm Solo daños Choque VIA 40 CALLE 75 nan nan 1 2018 January Mon
5 2018-01-01T00:00:00.000 06:00:00:pm Con heridos Choque CR 8 CL 41 3.000000 nan 1 2018 January Mon
6 2018-01-01T00:00:00.000 12:50:00:am Con heridos Atropello CLLE 119B CRA 11B 1.000000 nan 1 2018 January Mon
7 2018-01-02T00:00:00.000 02:30:00:pm Solo daños Choque CARRERA 25 37-42 nan nan 1 2018 January Tue
8 2018-01-02T00:00:00.000 03:00:00:pm Solo daños Choque CR 51B 1D 35 nan nan 1 2018 January Tue
9 2018-01-02T00:00:00.000 03:45:00:pm Solo daños Choque VIA 40 CLLE 85 nan nan 1 2018 January Tue

Análisis: características de la base de datos#

Esta base de datos, proporcionada por la Alcaldía Distrital de Barranquilla, contiene información relacionada con los accidentes de tránsito que han ocurrido en la ciudad desde 2018 hasta el 30 de junio de 2024, según los informes policiales de accidente de tránstito (IPAT). Cada una de las columnas, proporciona información específica sobre cada evento, como por ejemplo la fecha y lugar dónde ocurrió, la cantidad de heridos, gravedad del suceso, etc.

En este caso, contamos con 25610 observaciones, las cuales representan cada accidente, y con 11 variables. Veamos ahora, la información relacionada con el nombre y tipo de las variables.

df.info()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[3], line 1
----> 1 df.info()

NameError: name 'df' is not defined

Al ver la respuesta proporcionada por df.info(), observamos que indica que hay 4 variables de tipo numérica, sin embargo, según el sitio web que proporcionó la base de datos, la variable AÑO_ACCIDENTE, es tipo factor/caracter. Por tanto,

data['AÑO_ACCIDENTE'] = data['AÑO_ACCIDENTE'].astype('object')

Por tanto, podemos ahora afirmar que, de las 11 variables, 3 de ellas, CANT_HERIDOS_EN_SITIO_ACCIDENTE, CANT_MUERTOS_EN_SITIO_ACCIDENTE y CANTIDAD_ACCIDENTES, son numéricas y las 8 restantes son categóricas.

Análisis: variable según su tipo#

Numéricas:#

En primer lugar, veamos las principales estadísticas descriptivas de cada variable numérica.

data.describe()
CANT_HERIDOS_EN _SITIO_ACCIDENTE CANT_MUERTOS_EN _SITIO_ACCIDENTE CANTIDAD_ACCIDENTES
count 9984.000000 252.000000 25610.000000
mean 1.471655 1.035714 1.000195
std 1.156751 0.185946 0.013972
min 1.000000 1.000000 1.000000
25% 1.000000 1.000000 1.000000
50% 1.000000 1.000000 1.000000
75% 2.000000 1.000000 1.000000
max 42.000000 2.000000 2.000000

Ahora, grafiquemos cada una de ellas por separado para analizarlas.

CANT_HERIDOS_EN _SITIO_ACCIDENTE#

sns.boxplot(x=data['CANT_HERIDOS_EN _SITIO_ACCIDENTE'], color='lightblue')
plt.title('Boxplot de la cantidad de heridos')
plt.xlabel('Cantidad de Heridos')
plt.show()
_images/540a92bb11e930e3c44f58816ee3b47b8a52d34f98d441c8b60630580e8e85dd.png

Lo que podemos mencionar sobre esta variable es que el promedio de personas heridas es aproximadamente 1 por accidente, sin embargo, se nota que hay casos en donde esto varió y hubo un alto número de personas afectadas, como es visible en la gráfica la cual cuenta con una gran cantidad de datos atípicos.

CANT_MUERTOS_EN _SITIO_ACCIDENTE#

sns.boxplot(x=data['CANT_MUERTOS_EN _SITIO_ACCIDENTE'], color='lightblue')
plt.title('Boxplot de la cantidad de muertos')
plt.xlabel('Cantidad de muertos')
plt.show()
_images/367a84d7ed75c695710ad7521c47632d17e00bfbcce59a0833cd8c7f5cfc6546.png

Lo que podemos mencionar sobre esta variable es que el promedio de personas muertas es 1 por accidente, sin embargo, se nota que estos valores varían entre 1 y 2 dada la existencia del dato atípico.

CANTIDAD_ACCIDENTES#

sns.boxplot(x=data['CANTIDAD_ACCIDENTES'], color='lightblue')
plt.title('Boxplot de la cantidad de accidentes')
plt.xlabel('Cantidad de accidentes')
plt.show()
_images/f1df93b79e3c4d0822ef6d0ef7171e96c57d234e7dc40d6fa233c369ea316c8c.png

Lo que podemos mencionar sobre esta variable es que el promedio de accidentes por hora específica en cada fecha es 1, sin embargo, se nota que estos valores varían entre 1 y 2 dada la existencia del dato atípico.

Correlación#

Analicemos si existe alguna correlación entre estas.

data_num = data[['CANT_HERIDOS_EN _SITIO_ACCIDENTE', 'CANT_MUERTOS_EN _SITIO_ACCIDENTE','CANTIDAD_ACCIDENTES']]
matriz_correlacion = data_num.corr()
plt.figure(figsize=(12, 8)) # Configuración del tamaño de la figura
sns.heatmap(matriz_correlacion, annot = True, cmap = 'Blues', fmt = '.2f', vmin = -1, vmax = 1)
plt.title('Mapa de Calor de Correlaciones')
plt.show()
_images/6ece67ac8e2097c42988cacee31c940e5b39eda975877d02aebe28daea5e3f8c.png

Esto quiere decir que no hay ningun par de variables que cuente con una correlación significativa, por tanto, podemos afirmar que el comportamiento u ocurrencia de cada una de ellas es independiente a las demás.

Categóricas#

En primer lugar, veamos una descripción resumida de cada una.

data.describe(include = object)
FECHA_ACCIDENTE HORA_ACCIDENTE GRAVEDAD_ACCIDENTE CLASE_ACCIDENTE SITIO_EXACTO_ACCIDENTE AÑO_ACCIDENTE MES_ACCIDENTE DIA_ACCIDENTE
count 25610 25610 25610 25610 25610 25610 25610 25610
unique 2357 871 3 6 15365 7 12 7
top 2018-06-08T00:00:00.000 04:00:00:pm Solo daños Choque CL 110 CR 9G 2018 February Tue
freq 34 408 15457 23819 77 5898 2477 4009

Ahora, vamos a analizarlas con más profundidad.

FECHA_ACCIDENTE#

frec_fecha = data['FECHA_ACCIDENTE'].value_counts().sort_values(ascending=False)
frec_fecha
FECHA_ACCIDENTE
2018-06-08T00:00:00.000    34
2018-11-16T00:00:00.000    33
2019-05-22T00:00:00.000    31
2019-10-05T00:00:00.000    30
2020-02-13T00:00:00.000    29
                           ..
2020-07-05T00:00:00.000     1
2024-02-07T00:00:00.000     1
2020-08-16T00:00:00.000     1
2020-08-10T00:00:00.000     1
2020-08-09T00:00:00.000     1
Name: count, Length: 2357, dtype: int64

Es decir, el día que hubo mayor cantidad de accidentes fue el 8 de junio del año 2018, con 34 incidentes.

HORA_ACCIDENTE#

frec_hora = data['HORA_ACCIDENTE'].value_counts().sort_values(ascending=False)
frec_hora
HORA_ACCIDENTE
03:00:00:pm    408
04:00:00:pm    408
12:30:00:pm    387
05:00:00:pm    384
08:00:00:am    384
              ... 
04:43:00:pm      1
01:06:00:am      1
01:56:00:pm      1
05:23:00:pm      1
07:24:00:pm      1
Name: count, Length: 871, dtype: int64

Es decir que, según los datos recolectados, los accidentes tendían a ocurrir en su mayoría en horas de la tarde, más específicamente a las 3pm y 4pm.

GRAVEDAD_ACCIDENTE#

frec_gravedad = data['GRAVEDAD_ACCIDENTE'].value_counts()
frec_gravedad
GRAVEDAD_ACCIDENTE
Solo daños     15457
Con heridos     9901
Con muertos      252
Name: count, dtype: int64
ax = sns.catplot(data = data, x = 'GRAVEDAD_ACCIDENTE', aspect = 1.5, kind = "count", color = "lightblue")
ax.set_axis_labels('Gravedad del accidente', 'Número de accidentes')
ax.fig.suptitle('Frecuencia del tipo de gravedad de accidente', fontsize = 16)
sns.set_style("whitegrid") 
plt.subplots_adjust(top=0.9) # Ajusta el espacio para el título
plt.grid(True, which = 'both', linestyle = '--', linewidth = 0.5)
plt.show()
_images/1f98d0870f9e3e459b93e9db2ca141cd380521edab8d5ffb83f41c8d7e833521.png

A partir de este diagrama de barras y las frecuencias calculadas, se puede afirmar que la mayoría de los accidentes no tuvieron consecuencias graves para las personas, ya que en su mayoría solo se reportaron daños materiales. Además, el número de fallecidos es significativamente menor en comparación con los otros dos casos: Con heridos y Solo daños.

CLASE_ACCIDENTE#

data['CLASE_ACCIDENTE'].value_counts()
CLASE_ACCIDENTE
Choque            23819
Atropello          1344
Caida Ocupante      194
Otro                123
Volcamiento         117
Incendio             13
Name: count, dtype: int64
ax = sns.catplot(data = data, x = 'CLASE_ACCIDENTE', aspect = 1.5, kind = "count", color = "lightblue")
ax.set_axis_labels('Clase de accidente', 'Número de accidentes')
ax.fig.suptitle('Frecuencia de cada clase de accidente', fontsize=16)
sns.set_style("whitegrid")
plt.subplots_adjust(top = 0.9)
plt.grid(True, which ='both', linestyle = '--', linewidth = 0.5)
plt.show()
_images/0343898b18ac2ccff8c0b71aed6781a1bc5f5676d00e953c877925be04e3fea6.png

A partir de esta gráfica y las frecuencias calculadas, se puede evidenciar con claridad que la mayor cantidad de accidentes se debe, de manera significativa, al tipo Choque. El resto de las clases de accidentes se distribuye casi de manera proporcional entre las demás categorías, considerando la frecuencia de Atropello ligeramente mayor.

año_accidente#

data['AÑO_ACCIDENTE'].value_counts()
AÑO_ACCIDENTE
2018    5898
2019    5645
2021    4700
2022    3683
2020    3281
2023    1662
2024     741
Name: count, dtype: int64
ax = sns.catplot(data = data, x = 'AÑO_ACCIDENTE', aspect = 1.5, kind = "count", color = "lightblue")
ax.set_axis_labels('Año de ocurrencia', 'Número de accidentes')
ax.fig.suptitle('Frecuencia de año de ocurrencia de accidente', fontsize=16)
sns.set_style("whitegrid")
plt.subplots_adjust(top = 0.9)
plt.grid(True, which ='both', linestyle = '--', linewidth = 0.5)
plt.show()
_images/b6fbb57be819662eeba5770f11fb6e4d2554c8d440b5c1d6836539b6b50664b4.png

Se puede observar que desde el año 2018 hasta el año actual, la tendencia de ocurrencia de accidentes ha disminuido con una variablidad un tanto significativa. El año con el mayor número de accidentes registrados, 5898, fue 2018, mientras que 2024 tuvo la menor frecuencia, 741. A su vez, siguiendo la idea del principio, se nota que a partir del 2021, se mantiene un patrón de descenso continuo en la frecuencia de accidentes en la ciudad de Barranquilla.

MES_ACCIDENTE#

data['MES_ACCIDENTE'].value_counts()
MES_ACCIDENTE
February     2477
March        2446
January      2349
December     2189
May          2121
June         2103
October      2090
April        2010
November     1995
September    1980
July         1932
August       1918
Name: count, dtype: int64
ax = sns.catplot(data = data, x = 'MES_ACCIDENTE', aspect = 1.5, kind = "count", color = "lightblue")
ax.set_axis_labels('Mes de ocurrencia', 'Número de accidentes')
ax.fig.suptitle('Frecuencia del mes de ocurrencia del accidente', fontsize=16)
sns.set_style("whitegrid")
plt.subplots_adjust(top = 0.9)
plt.xticks(rotation = 45, ha = 'right')
plt.grid(True, which = 'both', linestyle = '--', linewidth = 0.5)
plt.show()
_images/6cdc8bf906900e6fdb9f894c15dab0a03591c24d640b584202980eeddf4b270b.png

Se puede observar que la cantidad de accidentes por mes es bastante similar, con valores cercanos a aproximadamente 2000. El mes con el mayor número de accidentes acumulados es febrero, con 2477 accidentes, mientras que el mes con el menor número es agosto, con 1918 registros.

DIA_ACCIDENTE#

data['DIA_ACCIDENTE'].value_counts()
DIA_ACCIDENTE
Tue    4009
Fri    3920
Wed    3839
Mon    3774
Thu    3756
Sat    3735
Sun    2577
Name: count, dtype: int64
ax = sns.catplot(data = data, x = 'DIA_ACCIDENTE', aspect = 1.5, kind = "count", color = "lightblue")
ax.set_axis_labels('Día de ocurrencia', 'Número de accidentes')
ax.fig.suptitle('Frecuencia de día de ocurrencia del accidente', fontsize = 16)
sns.set_style("whitegrid")
plt.subplots_adjust(top = 0.9)
plt.grid(True, which = 'both', linestyle = '--', linewidth = 0.5)
plt.show()
_images/c7f49d2d60797430b8bfb29218110acfdd7b858e4bcdac0d7b2e08b1514e4708.png

En este gráfico, se puede observar que la frecuencia de accidentes es bastante parecida entre la mayoría de los días de la semana, con la excepción notable del domingo, que tiene una frecuencia visiblemente menor. Según los registros de este estudio, el día con la mayor cantidad de accidentes es el martes, con 4009 incidentes, mientras que, como ya se mencionó, el domingo tiene la menor cantidad, con 2577 accidentes.

Filtrado de la base de datos#

Para esta sección, vamos a filtrar la base de datos con diferentes casos y poder así explorar los datos.

1. Accidentes de tipo choque ocurridos en los años 2018 y 2019#

Haremos un breve análisis de este subconjunto usando las variables DIA_ACCIDENTE, GRAVEDAD_ACCIDENTE y MES_ACCIDENTE.

filtro1 = data[(data['CLASE_ACCIDENTE'] == "Choque") & (data['AÑO_ACCIDENTE'].isin([2018, 2019]))]
display(filtro1.head(10).style.set_caption("Base de datos: Filtro 1"))
Base de datos: Filtro 1
  FECHA_ACCIDENTE HORA_ACCIDENTE GRAVEDAD_ACCIDENTE CLASE_ACCIDENTE SITIO_EXACTO_ACCIDENTE CANT_HERIDOS_EN _SITIO_ACCIDENTE CANT_MUERTOS_EN _SITIO_ACCIDENTE CANTIDAD_ACCIDENTES AÑO_ACCIDENTE MES_ACCIDENTE DIA_ACCIDENTE
1 2018-01-01T00:00:00.000 02:00:00:pm Solo daños Choque CL 110 CR 46 nan nan 1 2018 January Mon
2 2018-01-01T00:00:00.000 04:00:00:am Solo daños Choque AV CIRCUNVALAR CR 9G nan nan 1 2018 January Mon
3 2018-01-01T00:00:00.000 04:30:00:am Solo daños Choque CLLE 72 CRA 29 nan nan 1 2018 January Mon
4 2018-01-01T00:00:00.000 05:20:00:pm Solo daños Choque VIA 40 CALLE 75 nan nan 1 2018 January Mon
5 2018-01-01T00:00:00.000 06:00:00:pm Con heridos Choque CR 8 CL 41 3.000000 nan 1 2018 January Mon
7 2018-01-02T00:00:00.000 02:30:00:pm Solo daños Choque CARRERA 25 37-42 nan nan 1 2018 January Tue
8 2018-01-02T00:00:00.000 03:00:00:pm Solo daños Choque CR 51B 1D 35 nan nan 1 2018 January Tue
9 2018-01-02T00:00:00.000 03:45:00:pm Solo daños Choque VIA 40 CLLE 85 nan nan 1 2018 January Tue
10 2018-01-02T00:00:00.000 04:10:00:pm Solo daños Choque CL 19 CR 2C nan nan 1 2018 January Tue
11 2018-01-02T00:00:00.000 05:30:00:pm Solo daños Choque VIA 40 CL 77B nan nan 1 2018 January Tue
frec_diafiltro1 = filtro1['DIA_ACCIDENTE'].value_counts()
frec_diafiltro1
DIA_ACCIDENTE
Tue    1727
Fri    1680
Wed    1668
Thu    1626
Mon    1602
Sat    1577
Sun    1002
Name: count, dtype: int64

Al analizar este nuevo subconjunto con respecto a los días de la semana, podemos observar que el viernes sigue siendo el día con mayor cantidad de accidentes, en este caso, solo de tipo Choque.

frec_gravedadfiltro1 = filtro1['GRAVEDAD_ACCIDENTE'].value_counts()
frec_gravedadfiltro1
GRAVEDAD_ACCIDENTE
Solo daños     8111
Con heridos    2725
Con muertos      46
Name: count, dtype: int64

Durante los años 2018 y 2019, los accidentes clasificados como Choque resultaron mayoritariamente en daños materiales. Además, es importante destacar que la frecuencia de Con muertos en estos accidentes fue significativamente menor en comparación con el número de Con heridos y Solo daños.

frec_mesfiltro1 = filtro1['MES_ACCIDENTE'].value_counts()
frec_mesfiltro1
MES_ACCIDENTE
September    962
April        940
December     931
March        930
August       919
October      916
May          911
November     909
January      885
February     874
July         856
June         849
Name: count, dtype: int64

Podemos observar que los accidentes de tipo Choque durante los años 2018 y 2019 se distribuyeron de manera casi uniforme a lo largo de los 12 meses. La cantidad de incidentes por mes se aproxima a los 900. En comparación con la base de datos original, el mes con la menor cantidad de choques acumulados es junio, con 849 incidentes, mientras que el mes con la mayor frecuencia es septiembre, con 962 incidentes.

2. Accidentes ocurridos los días sábado y domingo en el primer trimestre de cada año sin muertos#

Haremos un breve análisis de este subconjunto usando las variables AÑO_ACCIDENTE y CLASE_ACCIDENTE.

filtro2 = data[
    (data['DIA_ACCIDENTE'].isin(["Sat", "Sun"])) & 
    (data['MES_ACCIDENTE'].isin(["January", "February", "March"])) & 
    (data['GRAVEDAD_ACCIDENTE'].isin(["Con heridos", "Solo daños"]))
]
display(filtro2.head(10).style.set_caption("Base de datos: Filtro 2"))
Base de datos: Filtro 2
  FECHA_ACCIDENTE HORA_ACCIDENTE GRAVEDAD_ACCIDENTE CLASE_ACCIDENTE SITIO_EXACTO_ACCIDENTE CANT_HERIDOS_EN _SITIO_ACCIDENTE CANT_MUERTOS_EN _SITIO_ACCIDENTE CANTIDAD_ACCIDENTES AÑO_ACCIDENTE MES_ACCIDENTE DIA_ACCIDENTE
58 2018-01-06T00:00:00.000 02:25:00:pm Con heridos Caida Ocupante AV CORDIALIDAD CON CIRCUNVALAR 1.000000 nan 1 2018 January Sat
59 2018-01-06T00:00:00.000 03:20:00:pm Solo daños Choque CR 44 CL 42 nan nan 1 2018 January Sat
60 2018-01-06T00:00:00.000 05:30:00:pm Solo daños Choque CALLE 70 CRA 48 nan nan 1 2018 January Sat
61 2018-01-06T00:00:00.000 07:50:00:pm Solo daños Choque CLLE 84 CRA 42B1 nan nan 1 2018 January Sat
62 2018-01-06T00:00:00.000 09:30:00:am Con heridos Atropello CLLE 110 CRA 6 2.000000 nan 1 2018 January Sat
63 2018-01-06T00:00:00.000 10:15:00:am Solo daños Choque VIA 40 CR 46 nan nan 1 2018 January Sat
64 2018-01-06T00:00:00.000 10:55:00:am Solo daños Choque CRA 46 CLLE 76 nan nan 1 2018 January Sat
65 2018-01-06T00:00:00.000 11:25:00:am Con heridos Choque CLLE 68 CRA 60 1.000000 nan 1 2018 January Sat
66 2018-01-06T00:00:00.000 11:30:00:am Con heridos Choque CRA 51B CALLE 90 1.000000 nan 1 2018 January Sat
67 2018-01-06T00:00:00.000 11:30:00:am Solo daños Choque CR 42F 75B 124 nan nan 1 2018 January Sat
frec_añofiltro2 = filtro2['AÑO_ACCIDENTE'].value_counts()
frec_añofiltro2
AÑO_ACCIDENTE
2019    346
2018    344
2022    292
2020    287
2021    275
2023    107
2024    105
Name: count, dtype: int64

En este subconjunto de datos, se puede observar un comportamiento similar al de la base de datos principal, con una ligera tendencia a la disminución de accidentes a lo largo de los años. Sin embargo, se destaca que en 2019 y 2022, la frecuencia de accidentes es ligeramente superior en comparación con el año anterior. El año con la mayor cantidad de accidentes, según los filtros establecidos, fue 2019, con 346 registros, mientras que el año con la menor cantidad fue 2024, con 105 registros.

frec_clasefiltro2 = filtro2['CLASE_ACCIDENTE'].value_counts()
frec_clasefiltro2
CLASE_ACCIDENTE
Choque            1619
Atropello          107
Caida Ocupante      13
Otro                 8
Volcamiento          7
Incendio             2
Name: count, dtype: int64

Para este resultado, se puede mencionar que la cantidad de accidentes de tipo Choque, según los filtros establecidos, es significativamente mayor en comparación con las demás clases de accidente. La cantidad de accidentes de esta clase es de 1619, mientras que la clase con la menor frecuencia es Incendio, con solo 2 registros. Este subconjunto, analizado desde la variable de clase_accidente, refleja el mismo comportamiento que la base de datos original, ya que la distribución de los accidentes en otras clases es relativamente igual, siendo la frecuencia de Atropello ligeramente superior.

3. Accidentes ocurridos entre 2019 y 2021 en febrero#

Haremos un breve análisis de este subconjunto usando las variables DIA_ACCIDENTE, GRAVEDAD_ACCIDENTE y CLASE_ACCIDENTE.

filtro3 = data[
    (data['AÑO_ACCIDENTE'].isin([2019, 2020, 2021])) & 
    (data['MES_ACCIDENTE'] == "February")
]
display(filtro3.head(10).style.set_caption("Base de datos: Filtro 3"))
Base de datos: Filtro 3
  FECHA_ACCIDENTE HORA_ACCIDENTE GRAVEDAD_ACCIDENTE CLASE_ACCIDENTE SITIO_EXACTO_ACCIDENTE CANT_HERIDOS_EN _SITIO_ACCIDENTE CANT_MUERTOS_EN _SITIO_ACCIDENTE CANTIDAD_ACCIDENTES AÑO_ACCIDENTE MES_ACCIDENTE DIA_ACCIDENTE
6379 2019-02-01T00:00:00.000 01:00:00:pm Con heridos Choque CL 8 41 43 1.000000 nan 1 2019 February Fri
6380 2019-02-01T00:00:00.000 01:00:00:pm Solo daños Choque CL 45 10C 08 nan nan 1 2019 February Fri
6381 2019-02-01T00:00:00.000 01:10:00:pm Solo daños Choque CL 90 53 45 nan nan 1 2019 February Fri
6382 2019-02-01T00:00:00.000 01:30:00:pm Solo daños Choque VIA 40 CL 85 nan nan 1 2019 February Fri
6383 2019-02-01T00:00:00.000 02:00:00:pm Solo daños Choque CL 19 CR 1 nan nan 1 2019 February Fri
6384 2019-02-01T00:00:00.000 02:50:00:pm Con heridos Choque VIA 40 CL 80 1.000000 nan 1 2019 February Fri
6385 2019-02-01T00:00:00.000 05:50:00:pm Con heridos Choque CR 15 SUR 78 123 1.000000 nan 1 2019 February Fri
6386 2019-02-01T00:00:00.000 06:00:00:pm Con heridos Otro CR 11 CL 35 1.000000 nan 1 2019 February Fri
6387 2019-02-01T00:00:00.000 06:30:00:am Solo daños Choque CL 53D 21 36 nan nan 1 2019 February Fri
6388 2019-02-01T00:00:00.000 06:30:00:pm Solo daños Choque CR 38 CL 31 nan nan 1 2019 February Fri
frec_diafiltro3 = filtro3['DIA_ACCIDENTE'].value_counts()
frec_diafiltro3
DIA_ACCIDENTE
Sat    213
Mon    203
Fri    196
Thu    182
Tue    179
Wed    179
Sun    118
Name: count, dtype: int64

Durante el mes de febrero en los años 2019, 2020 y 2021, se puede afirmar que el día con la mayor cantidad de accidentes fue el sábado, con 213 incidentes. Por otro lado, el domingo registró la menor cantidad de accidentes, con 118 registros, siendo este el único valor significativamente distante de los demás días, que se mantienen cerca de los 200 registros.

frec_gravedadfiltro3 = filtro3['GRAVEDAD_ACCIDENTE'].value_counts()
frec_gravedadfiltro3
GRAVEDAD_ACCIDENTE
Solo daños     894
Con heridos    372
Con muertos      4
Name: count, dtype: int64

Se puede decir que la mayor parte de los accidentes, según el filtro establecido, resultó en daños materiales, ya que la categoría Solo daños es la que cuenta con la mayor cantidad de incidentes, alcanzando 894 registros. Esto es significativamente más alto en comparación con las categorías Con heridos y Con muertos.

frec_clasefiltro3 = filtro3['CLASE_ACCIDENTE'].value_counts()
frec_clasefiltro3
CLASE_ACCIDENTE
Choque            1205
Atropello           52
Volcamiento          5
Otro                 4
Caida Ocupante       4
Name: count, dtype: int64

Este subconjunto, al igual que los anteriormente analizados, muestra una alta frecuencia en la clase de accidente Choque, que es significativamente mayor en comparación con las demás clases. Además, se observa que, nuevamente, la categoría Atropello tiene una frecuencia ligeramente superior a las otras, mientras que las demás clases de accidente presentan una distribución de datos más uniforme.

4. Accidentes de tipo atropello ocurridos en noviembre y diciembre ocurridos a las 6 am y 6 pm#

Haremos un breve análisis de este subconjunto usando las variables DIA_ACCIDENTE y GRAVEDAD_ACCIDENTE.

filtro4 = data[
    (data['CLASE_ACCIDENTE'] == "Atropello") & 
    (data['MES_ACCIDENTE'].isin(["November", "December"])) & 
    (data['HORA_ACCIDENTE'].isin(["06:00:00:am", "06:00:00:pm"]))
]
display(filtro4.head(10).style.set_caption("Base de datos: Filtro 4"))
Base de datos: Filtro 4
  FECHA_ACCIDENTE HORA_ACCIDENTE GRAVEDAD_ACCIDENTE CLASE_ACCIDENTE SITIO_EXACTO_ACCIDENTE CANT_HERIDOS_EN _SITIO_ACCIDENTE CANT_MUERTOS_EN _SITIO_ACCIDENTE CANTIDAD_ACCIDENTES AÑO_ACCIDENTE MES_ACCIDENTE DIA_ACCIDENTE
5396 2018-12-02T00:00:00.000 06:00:00:am Con heridos Atropello CALLE 45 CARRERA 15SUR 1.000000 nan 1 2018 December Sun
11126 2019-12-03T00:00:00.000 06:00:00:pm Con heridos Atropello CALLE 86 CRA 21 2.000000 nan 1 2019 December Tue
14572 2020-12-13T00:00:00.000 06:00:00:pm Con heridos Atropello AV CIRCUNVALAR CR 25 1.000000 nan 1 2020 December Sun
14631 2020-12-17T00:00:00.000 06:00:00:pm Con heridos Atropello CR 38 CL 77 1.000000 nan 1 2020 December Thu
24624 2023-11-02T00:00:00.000 06:00:00:pm Con heridos Atropello AVENIDA MURILLO CARRERA 11 SUR 1.000000 nan 1 2023 November Thu
frec_diafiltro4 = filtro4['DIA_ACCIDENTE'].value_counts()
frec_diafiltro4
DIA_ACCIDENTE
Sun    2
Thu    2
Tue    1
Name: count, dtype: int64

En este caso, se puede observar que solo 5 registros cumplen con los filtros establecidos. De estos, 1 ocurrió un martes, 2 un jueves y 2 un domingo.

frec_gravedadfiltro4 = filtro4['GRAVEDAD_ACCIDENTE'].value_counts()
frec_gravedadfiltro4
GRAVEDAD_ACCIDENTE
Con heridos    5
Name: count, dtype: int64

En cuanto a la gravedad de los accidentes, ambos registros resultaron en la categoría Con heridos, lo que explica que su frecuencia sea de 5.

df_filtrado4= df[(df['HORA_ACCIDENTE'].between('06:00:00 am', '12:00:00 pm')) &
              (df['CANTIDAD_ACCIDENTES'] == 2)]
df_filtrado4
FECHA_ACCIDENTE HORA_ACCIDENTE GRAVEDAD_ACCIDENTE CLASE_ACCIDENTE SITIO_EXACTO_ACCIDENTE CANT_HERIDOS_EN _SITIO_ACCIDENTE CANT_MUERTOS_EN _SITIO_ACCIDENTE CANTIDAD_ACCIDENTES AÑO_ACCIDENTE MES_ACCIDENTE DIA_ACCIDENTE
2602 2018-06-09T00:00:00.000 09:35:00:am Solo daños Choque CARRERA 38 CALLE 52 NaN NaN 2 2018 June Sat
12038 2020-02-06T00:00:00.000 09:00:00:am Solo daños Choque AV CIRCUNVALAR CR 27 NaN NaN 2 2020 February Thu
12837 2020-05-02T00:00:00.000 07:40:00:pm Solo daños Choque CR 46 87 27 NaN NaN 2 2020 May Sat

Este filtrado ha reducido el conjunto de datos a los accidentes ocurridos entre las 6:00 AM y las 12:00 PM que reportaron exactamente 2 accidentes en el sitio. En total se encontraron 3 observaciones que cumplen con estas condiciones.

df_filtrado5 = df[(df['DIA_ACCIDENTE'].isin(['Wed', 'Thu'])) &
             (df['AÑO_ACCIDENTE'].isin([2020, 2021])) &
             (df['HORA_ACCIDENTE'].between('06:00:00 am', '12:00:00 pm'))]
df_filtrado5.shape
(1296, 11)
df_filtrado5.head()
FECHA_ACCIDENTE HORA_ACCIDENTE GRAVEDAD_ACCIDENTE CLASE_ACCIDENTE SITIO_EXACTO_ACCIDENTE CANT_HERIDOS_EN _SITIO_ACCIDENTE CANT_MUERTOS_EN _SITIO_ACCIDENTE CANTIDAD_ACCIDENTES AÑO_ACCIDENTE MES_ACCIDENTE DIA_ACCIDENTE
11546 2020-01-01T00:00:00.000 08:00:00:pm Con heridos Choque CL 86 CR 9L 1.00 NaN 1 2020 January Wed
11547 2020-01-01T00:00:00.000 08:10:00:pm Solo daños Choque CL 72 CR 44 25 NaN NaN 1 2020 January Wed
11548 2020-01-01T00:00:00.000 09:00:00:pm Solo daños Choque CL 19 CR 2 NaN NaN 1 2020 January Wed
11549 2020-01-01T00:00:00.000 09:00:00:pm Solo daños Choque CR 6 89 87 NaN NaN 1 2020 January Wed
11550 2020-01-01T00:00:00.000 09:30:00:pm Solo daños Choque CL 72 CR 41 NaN NaN 1 2020 January Wed

Este filtrado ha reducido el conjunto de datos a los accidentes ocurridos los días miércoles y jueves durante los años 2020 y 2021, en la franja horaria de la mañana (entre las 6:00 AM y las 12:00 PM). En total se encontraron 1,296 observaciones que cumplen con estas condiciones.

5. Accidentes ocurridos a las 3, 4 y 5, de la mañana y tarde, en los años 2022 y 2023#

Haremos un breve análisis de este subconjunto usando las variables MES_ACCIDENTE, GRAVEDAD_ACCIDENTE y CLASE_ACCIDENTE.

filtro5 = data[
    (data['HORA_ACCIDENTE'].isin(["03:00:00:am", "04:00:00:am", "05:00:00:am", 
                                  "03:00:00:pm", "04:00:00:pm", "05:00:00:pm"])) & 
    (data['AÑO_ACCIDENTE'].isin([2022, 2023]))
]
display(filtro5.head(10).style.set_caption("Filtro 5"))
filtro5_mañana = filtro5[
    filtro5['HORA_ACCIDENTE'].isin(["03:00:00:am", "04:00:00:am", "05:00:00:am"])
]

filtro5_tarde = filtro5[
    filtro5['HORA_ACCIDENTE'].isin(["03:00:00:pm", "04:00:00:pm", "05:00:00:pm"])
]
frec_mesfiltro5mañana = filtro5_mañana['MES_ACCIDENTE'].value_counts()
frec_mesfiltro5mañana
MES_ACCIDENTE
June         7
April        6
March        5
August       5
February     4
July         4
November     4
December     4
May          2
September    2
January      1
Name: count, dtype: int64
frec_mesfiltro5tarde = filtro5_tarde['MES_ACCIDENTE'].value_counts()
frec_mesfiltro5tarde
MES_ACCIDENTE
March        37
April        31
February     30
June         28
July         27
January      24
May          19
August       14
December      8
November      8
September     7
October       6
Name: count, dtype: int64

Podemos observar una clara diferencia en la cantidad de accidentes entre la mañana y la tarde por mes en los años estudiados. A partir de estos datos, se puede afirmar que, al comparar las horas seleccionadas, era más probable que ocurrieran accidentes durante la tarde que en la mañana. Esto se evidencia en que la frecuencia más alta en el grupo de horas de la mañana es de 7 accidentes, mientras que en la tarde alcanza los 37.

frec_gravedadfiltro5mañana = filtro5_mañana['GRAVEDAD_ACCIDENTE'].value_counts()
frec_gravedadfiltro5mañana
GRAVEDAD_ACCIDENTE
Con heridos    26
Solo daños     12
Con muertos     6
Name: count, dtype: int64
frec_gravedadfiltro5tarde = filtro5_tarde['GRAVEDAD_ACCIDENTE'].value_counts()
frec_gravedadfiltro5tarde
GRAVEDAD_ACCIDENTE
Solo daños     120
Con heridos    114
Con muertos      5
Name: count, dtype: int64

Nuevamente, se observa una gran diferencia en las frecuencias de los tipos de consecuencias, excluyendo la categoría Con muertos, ya que para los dos grupos son prácticamente iguales. Según los resultados, se puede inferir que la cantidad de accidentes es mayor en la tarde dentro del grupo de horas escogidas, dado que la cantidad de registros por categoría de gravedad es significativamente mayor en la tarde comparado con la mañana.

frec_clasefiltro5mañana = filtro5_mañana['CLASE_ACCIDENTE'].value_counts()
frec_clasefiltro5mañana
CLASE_ACCIDENTE
Choque         38
Atropello       3
Volcamiento     2
Otro            1
Name: count, dtype: int64
frec_gravedadfiltro5tarde = filtro5_tarde['CLASE_ACCIDENTE'].value_counts()
frec_gravedadfiltro5tarde
CLASE_ACCIDENTE
Choque            221
Atropello          16
Caida Ocupante      1
Otro                1
Name: count, dtype: int64

Para ambos grupos, los accidentes de tipo Choque fueron los más frecuentes. Sin embargo, al igual que en los análisis previos, la frecuencia es significativamente mayor en el grupo de horas de la tarde.

Identificación de valores NA#

Para identificar la existencia de los datos Not Available en la base de datos, vamos a contarlos y graficarlos.

Primeramente, calculemos cuántos hay por columna.

cantidad_na = data.isna().sum()

for columna in cantidad_na.index:
    print(f"{columna}: {cantidad_na[columna]} NA's")
FECHA_ACCIDENTE: 0 NA's
HORA_ACCIDENTE: 0 NA's
GRAVEDAD_ACCIDENTE: 0 NA's
CLASE_ACCIDENTE: 0 NA's
SITIO_EXACTO_ACCIDENTE: 0 NA's
CANT_HERIDOS_EN _SITIO_ACCIDENTE: 15626 NA's
CANT_MUERTOS_EN _SITIO_ACCIDENTE: 25358 NA's
CANTIDAD_ACCIDENTES: 0 NA's
AÑO_ACCIDENTE: 0 NA's
MES_ACCIDENTE: 0 NA's
DIA_ACCIDENTE: 0 NA's

Es decir, que de las 11 variables, solo 2, CANT_HERIDOS_EN_ SITIO_ACCIDENTE y CANT_MUERTOS_EN_SITIO_ACCIDENTE, cuentan con datos faltantes. Nótese que, para la última mencionada, de las 25610 observaciones, 25318 son NA.

Ahora, veamos esto gráficamente.

porcentaje_NA = data.isna().mean() * 100

data_faltante = pd.DataFrame({'column': porcentaje_NA.index, 'percent_missing': porcentaje_NA.values})

data_faltante = data_faltante.sort_values(by='percent_missing')

plt.figure(figsize=(10, 8))
plt.barh(data_faltante['column'], data_faltante['percent_missing'], color='lightblue')
plt.xlabel('Porcentaje de datos faltantes')
plt.title('Porcentaje de datos faltantes por variable')
plt.show()
_images/7563c997b65452effe077424331a1f930ad9cf69ffa4a7077d7e908a27521a2e.png
msno.bar(data, color = "lightblue", sort = 'ascending')
plt.title('Datos Faltantes por Columna', fontsize = 16, fontweight = 'bold')
plt.xlabel('Columnas', fontsize = 14)
plt.ylabel('Porcentaje de Datos Faltantes', fontsize = 14)
plt.xticks(rotation=45) 
plt.tight_layout() 
plt.show()
_images/639d245d97f9dc89346ed99c6b3efc6a58554eba1e35d33747126f523d214a8e.png

En la primera gráfica, se muestra el porcentaje de datos que faltan en cada variable. Por ejemplo, alrededor del 55% de los datos en la variable CANT_HERIDOS_EN _SITIO_ACCIDENTE son faltantes, y casi el 100% de los datos en CANT_MUERTOS_EN _SITIO_ACCIDENTE también son NA.

La segunda gráfica confirma esta información, mostrando cuántos datos hay en total por cada columna, lo que ayuda a entender mejor cuáles son las variables con más datos faltantes, tal como se calculó anteriormente.

Ahora, debido a que el porcentaje de NA’s de la variable CANT_MUERTOS_EN _SITIO_ACCIDENTE es notablemente significativo, se puede borrar la columna.

data2 = data.drop(columns=["CANT_MUERTOS_EN _SITIO_ACCIDENTE"])

display(data2.head(10))
FECHA_ACCIDENTE HORA_ACCIDENTE GRAVEDAD_ACCIDENTE CLASE_ACCIDENTE SITIO_EXACTO_ACCIDENTE CANT_HERIDOS_EN _SITIO_ACCIDENTE CANTIDAD_ACCIDENTES AÑO_ACCIDENTE MES_ACCIDENTE DIA_ACCIDENTE
0 2018-01-01T00:00:00.000 01:30:00:am Con heridos Atropello CL 87 9H 24 1.0 1 2018 January Mon
1 2018-01-01T00:00:00.000 02:00:00:pm Solo daños Choque CL 110 CR 46 NaN 1 2018 January Mon
2 2018-01-01T00:00:00.000 04:00:00:am Solo daños Choque AV CIRCUNVALAR CR 9G NaN 1 2018 January Mon
3 2018-01-01T00:00:00.000 04:30:00:am Solo daños Choque CLLE 72 CRA 29 NaN 1 2018 January Mon
4 2018-01-01T00:00:00.000 05:20:00:pm Solo daños Choque VIA 40 CALLE 75 NaN 1 2018 January Mon
5 2018-01-01T00:00:00.000 06:00:00:pm Con heridos Choque CR 8 CL 41 3.0 1 2018 January Mon
6 2018-01-01T00:00:00.000 12:50:00:am Con heridos Atropello CLLE 119B CRA 11B 1.0 1 2018 January Mon
7 2018-01-02T00:00:00.000 02:30:00:pm Solo daños Choque CARRERA 25 37-42 NaN 1 2018 January Tue
8 2018-01-02T00:00:00.000 03:00:00:pm Solo daños Choque CR 51B 1D 35 NaN 1 2018 January Tue
9 2018-01-02T00:00:00.000 03:45:00:pm Solo daños Choque VIA 40 CLLE 85 NaN 1 2018 January Tue

Por otra parte, CANT_HERIDOS_EN _SITIO_ACCIDENTE puede considerarse una variable de tipo factor debido a que sus valores no son continuos, por lo tanto, realizar una imputación sobre ella no es realmente necesario. Esto se debe a que los NA’s en esta variable reflejan adecuadamente la información sobre las consecuencias del accidente. De hecho, observamos que los NA’s aparecen cuando la variable GRAVEDAD_ACCIDENTE toma los valores Con muertos o Solo daños, lo que indica que la ausencia de datos en CANT_HERIDOS_EN _SITIO_ACCIDENTE va de la mano con GRAVEDAD_ACCIDENTE.

El resultado muestra las 5 primeras y 5 últimas filas de los datos filtrados, donde se observa que todas las entradas cumplen con estas condiciones: no tienen información sobre heridos (NaN en la columna CANT_HERIDOS_EN _SITIO_ACCIDENTE) y corresponden a accidentes clasificados como “Solo daños” o “Con muertos”.

La imputación de valores faltantes en esta variable no es realmente necesaria, ya que se observa que los NA’s en esta variable tienden a aparecer cuando la variable GRAVEDAD_ACCIDENTE toma los valores “Con muertos” o “Solo daños”, lo que indica una relación directa entre la gravedad del accidente y la ausencia de datos en la variable CANT_HERIDOS_EN _SITIO_ACCIDENTE

Detección de valores atípicos#

Dado que las variables numéricas en este conjunto de datos pueden ser vistas y trabajadas como factores, la detección de valores atípicos no es la estrategia más efectiva. En vez de buscar anomalías, se optará por calcular tablas de frecuencias para analizar cómo se distribuyen las diferentes categorías. Este método proporcionará una visión clara de la distribución de los valores en estas variables discretas, facilitando un análisis más pertinente y ajustado a la naturaleza de los datos.

CANT_HERIDOS_EN_SITIO_ACCIDENTES#

frec_heridos = data2['CANT_HERIDOS_EN _SITIO_ACCIDENTE'].value_counts().reset_index()
frec_heridos.columns = ['CANT_HERIDOS_EN _SITIO_ACCIDENTE', 'n']  #
frec_heridos = frec_heridos.sort_values(by='n', ascending=False)


table = go.Figure(data=[go.Table(
    header=dict(values=list(frec_heridos.columns),
                fill_color='lightblue',
                align='left'),
    cells=dict(values=[frec_heridos['CANT_HERIDOS_EN _SITIO_ACCIDENTE'], frec_heridos['n']],
               fill_color='lightgrey',
               align='left'))
])

table.show()

Es evidente que hay una gran diferencia entre las frecuencias de los distintos valores que toma CANT_HERIDOS_EN_SITIO_ACCIDENTE.

CANTIDAD_ACCIDENTES#

frec_cantidad = data2['CANTIDAD_ACCIDENTES'].value_counts().reset_index()
frec_cantidad.columns = ['CANTIDAD_ACCIDENTES', 'n']  #
frec_cantidad = frec_cantidad.sort_values(by='n', ascending=False)


table = go.Figure(data=[go.Table(
    header=dict(values=list(frec_cantidad.columns),
                fill_color='lightblue',
                align='left'),
    cells=dict(values=[frec_cantidad['CANTIDAD_ACCIDENTES'], frec_cantidad['n']],
               fill_color='lightgrey',
               align='left'))
])

table.show()

Es muy notable la significante diferencia entre los 2 valores que toma la variable CANTIDAD_ACCIDENTES.

Precio del combustible en Colombia durante el 2023#

Para este ejercicio, hay que, en primer lugar, unir todas las bases de datos para convertirlas en una sola.

d1 = pd.read_csv('C:/Users/valec/Downloads/Escritorio\precios_t12023.csv')
d2 = pd.read_csv('C:/Users/valec/Downloads/Escritorio\precios_t22023.csv')
d3 = pd.read_csv('C:/Users/valec/Downloads/Escritorio\precios_t32023.csv')
d4 = pd.read_csv('C:/Users/valec/Downloads/Escritorio\precios_t42023.csv')

Base de datos#

data23 = pd.concat([d1, d2, d3, d4], axis=0, ignore_index=True)
display(data23.head(10).style.set_caption("Base de datos: Combustible en Colombia"))
Base de datos: Combustible en Colombia
  BANDERA NOMBRE COMERCIAL PRODUCTO FECHA REGISTRO DEPARTAMENTO MUNICIPIO VALOR PRECIO
0 TERPEL ESTACION DE SERVICIO SERVICENTRO LA PEDRERA DIESEL 01-Jan-2023 AMAZONAS LA PEDRERA 15000.000000
1 TERPEL ESTACION DE SERVICIO SERVICENTRO LA PEDRERA GASOLINA MOTOR 01-Jan-2023 AMAZONAS LA PEDRERA 15500.000000
2 TERPEL BALSA EL CONDOR GASOLINA MOTOR 01-Jan-2023 AMAZONAS LETICIA 11380.000000
3 TERPEL BALSA EL CONDOR DIESEL 01-Jan-2023 AMAZONAS LETICIA 10840.000000
4 TERPEL ESTACION DE SERVICIO DISTRIBUIDORA LOS COMUNEROS GASOLINA MOTOR 01-Jan-2023 AMAZONAS LETICIA 11380.000000
5 TERPEL ESTACION DE SERVICIO DISTRIBUIDORA LOS COMUNEROS GASOLINA MOTOR 01-Jan-2023 AMAZONAS LETICIA 11380.000000
6 TERPEL ESTACION DE SERVICIO DISTRIBUIDORA LOS COMUNEROS DIESEL 01-Jan-2023 AMAZONAS LETICIA 10671.000000
7 TEXACO EDS COMDECOM ABRIAQUI GASOLINA MOTOR 01-Jan-2023 ANTIOQUIA ABRIAQUÍ 11870.000000
8 TEXACO EDS COMDECOM ABRIAQUI DIESEL 01-Jan-2023 ANTIOQUIA ABRIAQUÍ 10910.000000
9 TEXACO ESTACIÓN DE SERVICIO Y MALL SANTA LUCIA S.A.S. DIESEL 01-Jan-2023 ANTIOQUIA AMAGÁ 9610.000000

Análisis: características de la base de datos#

Esta base de datos, proporcionada por la página oficial de SICOM (sistema de información de la cadena de distribución de combustibles del Ministerio de Minas y Energía), brinda información relacionada con los precios de los combustibles distribuidos alrededor de Colombia durante el año 2023. Cada una de las columnas, ofrece información específica, como por ejemplo la empresa, el tipo de combustible y el precio de este.

En este caso, contamos con 267943 observaciones y con 7 variables. Veamos ahora, la información relacionada con el nombre y tipo de las variables.

data23.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 267943 entries, 0 to 267942
Data columns (total 7 columns):
 #   Column            Non-Null Count   Dtype  
---  ------            --------------   -----  
 0   BANDERA           267943 non-null  object 
 1   NOMBRE COMERCIAL  267943 non-null  object 
 2   PRODUCTO          267943 non-null  object 
 3   FECHA REGISTRO    267943 non-null  object 
 4   DEPARTAMENTO      267943 non-null  object 
 5   MUNICIPIO         267943 non-null  object 
 6   VALOR PRECIO      267943 non-null  float64
dtypes: float64(1), object(6)
memory usage: 14.3+ MB

Podemos observar que de las 7 variables, solo 1, VALOR PRECIO, es de tipo numérica.

Análisis: variable según su tipo#

Numéricas#

VALOR PRECIO#

data23.describe()
VALOR PRECIO
count 2.679430e+05
mean 1.202313e+04
std 2.870979e+04
min 0.000000e+00
25% 9.350000e+03
50% 1.088000e+04
75% 1.384900e+04
max 1.475015e+07
sns.set_style("whitegrid") 
ax = sns.boxplot(data=data23, x='VALOR PRECIO', palette=['lightblue'], orient='h')
ax.set_xlim(0, 30000)
ax.set_xlabel('Precio')
ax.set_title('Precio del combustible')
plt.grid(True, which='both', linestyle='--', linewidth=0.5)
plt.show()
_images/9c7c642d29a34244e2b7d274b9c9f14300f1f7f17d274e09e24db7ffc07e3934.png

La variable muestra la presencia de valores extremos significativamente alejados de la mayoría de los datos. Según el análisis realizado, los precios del combustible en cada ubicación se concentran principalmente en el rango de 9000 a 14000 pesos, aproximadamente. En consecuencia, el valor extremo superior observado, 14750147, se destaca claramente como un dato atípico en comparación con el resto de los precios registrados.

Categóricas#

BANDERA#

frec_bandera = data23['BANDERA'].value_counts().reset_index()
frec_bandera.columns = ['BANDERA', 'n'] 
frec_bandera = frec_bandera.sort_values(by='n', ascending=False)

table = go.Figure(data=[go.Table(
    header=dict(values=list(frec_bandera.columns),
                fill_color='lightblue',
                align='left'),
    cells=dict(values=[frec_bandera['BANDERA'], frec_bandera['n']],
               fill_color='lightgrey',
               align='left'))
])

table.show()

Es decir que, entre todas las comercializadoras, Terpel es la que más distribuye los diferentes tipos de combustible en el país.

PRODUCTO#

data23.value_counts('PRODUCTO')
PRODUCTO
GASOLINA MOTOR    127338
DIESEL            108205
EXTRA              32400
Name: count, dtype: int64
ax = sns.catplot(data = data23, x = 'PRODUCTO', aspect = 1.5, kind = "count", color = "lightblue")
ax.set_axis_labels('Tipo de combustible', 'Frecuencia')
ax.fig.suptitle(' Frecuencia del tipo de combustible', fontsize = 12)
sns.set_style("whitegrid") 
plt.subplots_adjust(top = 0.9) 
plt.grid(True, which = 'both', linestyle = '--', linewidth = 0.5)
plt.show()
_images/c23eaa96a3ab3f24c255820eda000c0a96d429eab848e0a91383e03a59806c05.png

Se observa que la distribución del combustible tipo Extra es considerablemente inferior en comparación con los otros dos tipos. Además, es relevante mencionar que las frecuencias de Diesel y Gasolina Motor no difieren significativamente entre sí y que, entre ambos, el combustible más comercializado en Colombia durante el año 2023 fue la Gasolina Motor, con un total de 127338 registros.

DEPARTAMENTO#

frec_dpto = data23['DEPARTAMENTO'].value_counts().reset_index()
frec_dpto.columns = ['DEPARTAMENTO', 'n'] 
frec_dpto = frec_dpto.sort_values(by='n', ascending=False)

table = go.Figure(data=[go.Table(
    header=dict(values=list(frec_dpto.columns),
                fill_color='lightblue',
                align='left'),
    cells=dict(values=[frec_dpto['DEPARTAMENTO'], frec_dpto['n']],
               fill_color='lightgrey',
               align='left'))
])

table.show()

Por lo tanto, podemos afirmar que el departamento en donde más se comercializó los 3 tipos de combustible durante el 2023 fue en Nariño, con 31054 registros.

Identificación de valores NA#

Primeramente, calculemos de manera matemática la cantidad de datos faltantes.

cantidad_na_23 = data23.isna().sum()

for columna, cantidad in cantidad_na_23.items():
    print(f"{columna}: {cantidad} NA's")
BANDERA: 0 NA's
NOMBRE COMERCIAL: 0 NA's
PRODUCTO: 0 NA's
FECHA REGISTRO: 0 NA's
DEPARTAMENTO: 0 NA's
MUNICIPIO: 0 NA's
VALOR PRECIO: 0 NA's

Es decir que la base de datos no contiene ningún dato de tipo NA.

porcentaje_NA23 = data23.isna().mean() * 100

data_faltante23 = pd.DataFrame({'column': porcentaje_NA23.index, 'percent_missing': porcentaje_NA23.values})

data_faltante23 = data_faltante23.sort_values(by='percent_missing', ascending=False)

plt.figure(figsize=(10, 8))
plt.barh(data_faltante23['column'], data_faltante23['percent_missing'], color='lightblue')
plt.xlabel('Porcentaje de datos faltantes')
plt.title('Porcentaje de datos faltantes por variable')
plt.show()
_images/8dfbdc2f44d8eba7ce0cf78d34af9bf2765be607b47b83e0bdc6fec38a356c19.png

La gráfica muestra que el porcentaje de valores NA para todas las variables es 0.

Detección de datos atípicos#

Hagamos uso de la herramienta boxplot para identificar la existencia de outliers en nuestra variable de interés, VALOR PRECIO, por cada tipo de combustible.

plt.figure(figsize=(12, 6))

ax = sns.boxplot(data=data23, x='PRODUCTO', y='VALOR PRECIO', palette=['lightblue'])

ax.set(ylim=(0, 30000))

ax.set_xlabel('Tipo de Producto')
ax.set_ylabel('Valor Precio')
ax.set_title('Boxplot de Precio por Tipo de Producto')

plt.grid(True, which='both', linestyle='--', linewidth=0.5)

plt.tight_layout()
plt.show()
_images/61e92971afe21826e911ff22ba800c1d9762fb6dbd4fe54cd2fc8bc36273ffa7.png

A partir de esto, observamos que hay una evidente existencia de datos atípicos en la variable VALOR PRECIO. Por tanto, se procederá a realizar la imputación de estos.

Verifiquemos la normalidad de la variable

ks_test = stats.kstest(data23['VALOR PRECIO'], 'norm', args=(data23['VALOR PRECIO'].mean(), data23['VALOR PRECIO'].std()))
if ks_test.pvalue > 0.05:
    print("La variable 'VALOR PRECIO' sigue una distribución normal.")
else:
    print("La variable 'VALOR PRECIO' NO sigue una distribución normal.")

print(ks_test.pvalue)
La variable 'VALOR PRECIO' NO sigue una distribución normal.
0.0

Al hacer la prueba de Kolmogorov-Smirnov, se obtuvo un valor inferior a 0.05, esto indica que los datos no siguen una distribución normal. Es decir, la imputación debe realizarse haciendo uso de la mediana.

Q1 = data23['VALOR PRECIO'].quantile(0.25)
Q3 = data23['VALOR PRECIO'].quantile(0.75)

IQR = Q3 - Q1

lim_inf = Q1 - 1.5 * IQR
lim_sup = Q3 + 1.5 * IQR

mediana = data23['VALOR PRECIO'].median()

data23['VALOR PRECIO'] = np.where((data23['VALOR PRECIO'] < lim_inf) | (data23['VALOR PRECIO'] > lim_sup), mediana, data23['VALOR PRECIO'])

Veamos si la distribución se mantuvo

ks_test = stats.kstest(data23['VALOR PRECIO'], 'norm', args=(data23['VALOR PRECIO'].mean(), data23['VALOR PRECIO'].std()))
if ks_test.pvalue > 0.05:
    print("La variable 'VALOR PRECIO' sigue una distribución normal.")
else:
    print("La variable 'VALOR PRECIO' NO sigue una distribución normal.")
print(ks_test.pvalue)
La variable 'VALOR PRECIO' NO sigue una distribución normal.
0.0

La imputación fue realizada de correctamente pues la distribución de la variable VALOR PRECIO se mantuvo.

Filtración de la base de datos#

Para este ejercicio, vamos a filtrar la base según un solo tipo de combustible: Gasolina Motor y luego procederemos a analizarla con respecto a las diferentes columnas.

data_23_1 = data23[data23['PRODUCTO'] == 'GASOLINA MOTOR']
display(data_23_1.head(10).style.set_caption("Base de datos: Combustible de tipo Gasolina Motor"))
Base de datos: Combustible de tipo Gasolina Motor
  BANDERA NOMBRE COMERCIAL PRODUCTO FECHA REGISTRO DEPARTAMENTO MUNICIPIO VALOR PRECIO
1 TERPEL ESTACION DE SERVICIO SERVICENTRO LA PEDRERA GASOLINA MOTOR 01-Jan-2023 AMAZONAS LA PEDRERA 15500.000000
2 TERPEL BALSA EL CONDOR GASOLINA MOTOR 01-Jan-2023 AMAZONAS LETICIA 11380.000000
4 TERPEL ESTACION DE SERVICIO DISTRIBUIDORA LOS COMUNEROS GASOLINA MOTOR 01-Jan-2023 AMAZONAS LETICIA 11380.000000
5 TERPEL ESTACION DE SERVICIO DISTRIBUIDORA LOS COMUNEROS GASOLINA MOTOR 01-Jan-2023 AMAZONAS LETICIA 11380.000000
7 TEXACO EDS COMDECOM ABRIAQUI GASOLINA MOTOR 01-Jan-2023 ANTIOQUIA ABRIAQUÍ 11870.000000
10 TEXACO ESTACIÓN DE SERVICIO Y MALL SANTA LUCIA S.A.S. GASOLINA MOTOR 01-Jan-2023 ANTIOQUIA AMAGÁ 11200.000000
11 TERPEL ESTACION DE SERVICIO PUERTAS DEL NORDESTE GASOLINA MOTOR 01-Jan-2023 ANTIOQUIA BARBOSA 10560.000000
14 TERPEL ESTACION DE SERVICIO POPALITO GASOLINA MOTOR 01-Jan-2023 ANTIOQUIA BARBOSA 10399.000000
17 PRIMAX ESTACION DE SERVICIO PRIMAX AUTOPISTA GASOLINA MOTOR 01-Jan-2023 ANTIOQUIA BELLO 10300.000000
18 TERPEL TERPEL FONTIDUENO GASOLINA MOTOR 01-Jan-2023 ANTIOQUIA BELLO 10870.000000

Debido a que existen varios tipos de valores en BANDERA, vamos a analizarlo con los 4 de mayor frecuencia encontrados con la base original, con el fin de observar si hubo algún cambio en el comportimiento. Estos son Terpel, Primax, Biomax y Texaco.

empresas = ["TERPEL", "PRIMAX", "BIOMAX", "TEXACO"]
empresa = data_23_1[data_23_1['BANDERA'].isin(empresas)]

frec_empresa = pd.crosstab(empresa['BANDERA'], empresa['PRODUCTO'])

ax = frec_empresa.plot(kind='bar', figsize=(10, 6), color='lightblue', edgecolor='black', width=0.8, legend=False)
plt.title("Frecuencia de empresas por Gasolina Motor")
plt.xlabel("Empresa")
plt.ylabel("Frecuencia")
plt.xticks(rotation=0) 

plt.tight_layout()
plt.show()
_images/9333e518c92fb58a627a6dd1b230b17cc692db6563e3324da02e186e98745aff.png

Es posible observar que, una vez más, la bandera Terpel es la que más comercializó combustible, en este caso de tipo Gasolina Motor, mientras que la que menos lo hizo fue Texaco. Por otro lado, se evidencia un cambio en el comportamiento entre Biomax y Primax pues para este producto, Biomax presenta una mayor frecuencia de comercialización. Sin embargo, al analizar los tres tipos de combustibles conjuntamente, Primax es la que se distribuyó con mayor frecuencia entre ambas empresas.

Realizaremos esto mismo con los 4 valores de DEPARTAMENTO con mayor frecuencia.

departamentos = ["NARIÑO", "ANTIOQUIA", "NORTE DE SANTANDER", "VALLE DEL CAUCA"]
departamento = data_23_1[data_23_1['DEPARTAMENTO'].isin(departamentos)]

frec_dpto = pd.crosstab(departamento['DEPARTAMENTO'], departamento['PRODUCTO'])

ax = frec_dpto.plot(kind='bar', figsize=(10, 6), color='lightblue', edgecolor='black', width=0.8, legend=False)
plt.title("Frecuencia de departamentos por Gasolina Motor")
plt.xlabel("Departamento")
plt.ylabel("Frecuencia")
plt.xticks(rotation=0) 

plt.tight_layout()
plt.show()
_images/4f2bfdd125fe60fae7ed794e2b2159664b5a0590e3c0355ce1c05752d25c09a7.png

Observamos que este subconjunto tiene el mismo comportamiento de la base de datos original para estos 4 departamentos.

Ahora, veamos cómo se comporta el precio de este tipo de combustible por departamento.

result = (data_23_1.groupby('DEPARTAMENTO')
                .agg(precio_promedio=('VALOR PRECIO', 'mean'))
                .reset_index()
                .sort_values(by='precio_promedio'))

# Crear la tabla con plotly
table = go.Figure(data=[go.Table(
    header=dict(values=list(result.columns),
                fill_color='lightblue',
                align='left'),
    cells=dict(values=[result[col] for col in result.columns],
               fill_color='lightgrey',
               align='left'))
])

table.update_layout(title='Precio Promedio por Departamento')
table.show()

Creemos un rango para definir el nivel en el que se encuentra el promedio, esto servirá más adelante para realizar el mapa.

bins = [10000, 12000, 14000, 16000]
labels = ["Bajo", "Medio", "Alto"]
result['rango_precio'] = pd.cut(result['precio_promedio'], bins=bins, labels=labels, right=False)

# Ordenar por el rango_precio
rangos = result.sort_values(by='rango_precio')

table = go.Figure(data=[go.Table(
    header=dict(values=list(rangos.columns),
                fill_color='lightblue',
                align='left'),
    cells=dict(values=[result[col] for col in result.columns],
               fill_color='lightgrey',
               align='left'))
])

table.update_layout(title='Precio Promedio por Departamento')
table.show()

Se puede concluir que la mayoría de departamentos se encuentra en el rango establecido Medio, pues de los 32, solo NARIÑO y CESAR son de tipo BAJO, y en el caso de tipo ALTO solo encontramos a SAN ANDRES, GUAVIARE, VAUPES, GUAINIA y AMAZONAS.

Mapa de geolocalización#

Prcoderemos a graficar con la ayuda de un mapa los rangos creados en la sección anterior, con el fin de visualizar la distribución de precio promedio del combustible en Colombia.

Primeramente, vamos a leer el archivo Shapefile de Colombia y haremos un ajuste de nombre para el departamento de Nariño para evitar posibles errores.

mapa_col = gpd.read_file("C:/Users/valec/Downloads/Python _Act/Python _Act/coordenadas/COLOMBIA/COLOMBIA.shp")
mapa_col.loc[mapa_col['DPTO_CNMBR'] == 'NARI?O', 'DPTO_CNMBR'] = 'NARIÑO'

Ahora, vamos a unir la base que contiene a los rangos junto con la del archivo de coordenadas.

data_precios = data_23_rangos.groupby('DEPARTAMENTO')['VALOR PRECIO'].mean().reset_index()
data_precios['VALOR PRECIO'] = pd.to_numeric(df_precios['VALOR PRECIO'], errors='coerce')

# Unir los datos geoespaciales con precios
mapa_col = mapa_col.merge(data_precios[['DEPARTAMENTO', 'VALOR PRECIO']], 
                          left_on='DPTO_CNMBR', 
                          right_on='DEPARTAMENTO', 
                          how='left')

bins = [10000, 12000, 14000, 16000]
labels = ["Bajo", "Medio", "Alto"]
mapa_col['rango_precio'] = pd.cut(mapa_col['VALOR PRECIO'], bins=bins, labels=labels, right=False)

Y ahora, haciendo uso de la librería folium, vamos a graficar el mapa.

m = folium.Map(location=[4.5709, -74.2973], zoom_start=6)  # Centro de Colombia

color_map = {
    'Bajo': 'lightgreen',
    'Medio': 'lightyellow',
    'Alto': 'lightcoral'
}

for _, row in mapa_col.iterrows():
    folium.GeoJson(
        row['geometry'],
        style_function=lambda feature, color=color_map.get(row['rango_precio']): {
            'fillColor': color,
            'color': 'black',
            'weight': 0.5,
            'fillOpacity': 0.7
        },
        tooltip=folium.Tooltip(f"{row['DEPARTAMENTO']}: {row['rango_precio']}")
    ).add_to(m)
    
m.get_root().html.add_child(folium.Element(legend_html))

display(m)
Make this Notebook Trusted to load map: File -> Trust Notebook